webpack打包 您所在的位置:网站首页 webpack plugin上传oss缺失图片 webpack打包

webpack打包

2023-10-03 19:44| 来源: 网络整理| 查看: 265

@TOC# 什么是 webpack?

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。

webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。 webpack 通过 Tapable 来组织这条复杂的生产线。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。 – 《深入浅出 webpack》 吴浩麟

webpack 核心概念:

Entry:入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。

进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。

每个依赖项随即被处理,最后输出到称之为 bundles 的文件中。

Output:output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist。

基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。

Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。

Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。

Loader:loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。 本质上,webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块。

Plugins: loader 被用于转换某些类型的模块,而 plugins(插件)则可以用于执行范围更广的任务。 插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。

webpack 的核心机制(loader、plugins): Loader工作原理:

loader 是用来加载处理各种形式的资源的机制,本质上是一个函数, 接受文件作为参数,返回转化后的结构。

loader 是运行在 NodeJS 中的。因为 webpack 不认识一些外来模块,所以要使用一些加载器,比如识别 css/react/vue/png 等。

loader 虽然是扩展了 webpack ,但是它只专注于转化文件(transform)这一个领域,完成压缩,打包,语言翻译。

例如:css-loader 和 style-loader 模块是为了打包 css 的 babel-loader 和 babel-core 模块时为了把 ES6 的代码转 ES5 url-loader 和 file-loader 是把图片进行打包的。

用 webpack 源码中的代码来理解 loader 的工作原理: 模拟 style-loader 的功能,loader 的简单实现: //将css插入到head标签内部 module.exports = function (source) { let script = (` let style = document.createElement("style"); style.innerText = ${JSON.stringify(source)}; document.head.appendChild(style); `); return script; } //使用方式1 resolveLoader: { modules: [ path.resolve('node_modules'), path.resolve(__dirname, 'src', 'loaders')] }, { test: /\.css$/, use: ['style-loader'] }, } //使用方式2 //将自己写的loaders发布到npm仓库,然后添加到依赖,按照方式1中的配置方式使用即可 以下代码是 webpack 源码中 loader 执行关键步骤,以递归的方式执行 loader,执行机制流程似于 express 中间件机制: function iteratePitchingLoaders(options, loaderContext, callback) { var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex]; // load loader module loadLoader(currentLoaderObject, function (err) { var fn = currentLoaderObject.pitch; runSyncOrAsync( fn, loaderContext, [ loaderContext.remainingRequest, loaderContext.previousRequest, (currentLoaderObject.data = {}), ], function (err) { if (err) return callback(err); var args = Array.prototype.slice.call(arguments, 1); if (args.length > 0) { loaderContext.loaderIndex--; iterateNormalLoaders(options, loaderContext, args, callback); } else { iteratePitchingLoaders(options, loaderContext, callback); } } ); }); } plugin 工作原理:

plugin 是一个具有 apply 方法的 js 对象。 apply 方法会被 webpack 的 compiler(编译器)对象调用,并且 compiler 对象可在整个 compilation(编译)生命周期内访问。

plugins 是作用于 webpack 本身上的。webpack 提供了很多开箱即用的插件 插件可以携带参数,所以可以在 plugins 属性传入 new 实例:

1)CommonChunkPlugin 主要用于提取第三方库和公共模块,避免首屏加载的 bundle 文件,或者按需加载的 bundle 文件体积过大,导致加载时间过长,是一把

优化的利器。而在多页面应用中,更是能够为每个页面间的应用程序共享代码创建 bundle。

2)针对 html 文件打包和拷贝(还有很多设置)的插件:html-webpack-plugin

其不但完成了 html 文件的拷贝,打包,还给 html 中自动增加了引入打包后的 js 文件的代码(),还能指明把 js 文 件引入到 html 文件的底部等等。

代码如下:

plugins: [ // 对html模板进行处理,生成对应的html,引入需要的资源模块 new HtmlWebpackPlugin({ template: "./index.html", // 模板文件,即需要打包和拷贝到build目录下的html文件 filename: "index.html", // 目标html文件 chunks: ["useperson"], // 对应加载的资源,即html文件需要引入的js模块 inject: true, // 资源加入到底部,把模块引入到html文件的底部 }), ]; webpack 中 plugins 的组成: 一个 JavaScript 函数或者 class(ES6 语法)。在它的原型上定义一个 apply 方法。指定挂载的 webpack 事件钩子。处理 webpack 内部实例的特定数据。功能完成后调用 webpack 提供的回调。以常用的插件 UglifyJsPlugin 为分析示例: class UglifyJsPlugin { apply(compiler) { const options = this.options; options.test = options.test || /\.js($|\?)/i; ...... //绑定compilation事件 compiler.plugin("compilation", (compilation) => { if(options.sourceMap) { compilation.plugin("build-module", (module) => { // to get detailed location info about errors module.useSourceMap = true; }); } //绑定optimize-chunk-assets事件 compilation.plugin("optimize-chunk-assets", (chunks, callback) => { const files = []; chunks.forEach((chunk) => files.push.apply(files, chunk.files)); ...... callback(); }); }); } } module.exports = UglifyJsPlugin; webpack 的核心构建流程:

图1

这个过程核心完成了 内容转换 + 资源合并 两种功能,在实现上包含三个阶段:

1、初始化阶段:

初始化参数:从配置文件、 配置对象、Shell 参数中读取,与默认配置结合得出最终的参数

创建编译器对象:用上一步得到的参数创建 Compiler 对象

初始化编译环境:包括注入内置插件、注册各种模块工厂、初始化 RuleSet 集合、加载配置的插件等

开始编译:执行 compiler 对象的 run 方法

确定入口:根据配置中的 entry 找出所有的入口文件,调用 compilition.addEntry 将入口文件转换为 dependence 对象

2、构建阶段:

编译模块(make):根据 entry 对应的 dependence 创建 module 对象,调用 loader 将模块转译为标准 JS 内容,调用 JS 解释器将内容转换为 AST 对象,从中找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理

完成模块编译:上一步递归处理所有能触达到的模块后,得到了每个模块被翻译后的内容以及它们之间的 依赖关系图

3、生成阶段:

输出资源(seal):根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会

写入文件系统(emitAssets):在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

可理解为 Webpack 的运行流程是一个串行的过程,从启动到结束依次执行的流程 如下: 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。

开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。

确定入口:根据配置中的 entry 找出所有的入口文件。

编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。

完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。

输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。

输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

看图加深理解 webpack 打包流程,如下图所示:

图2

图示流程理解分析:

读取入口文件;基于 AST(抽象语法树) 分析入口文件,并产出依赖列表;AST (Abstract Syntax Tree)抽象语法树 在计算机科学中,或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。 (https://astexplorer.net/)使用 Babel 将相关模块编译到 ES5;webpack 有一个智能解析器(各种 babel),几乎可以处理任何第三方库。无论它们的模块形式是 CommonJS、AMD 还是普通的 JS 文件;甚至在加载依赖的时候,允许使用动态表 require(“、/templates/”+name+“、jade”)。以下这些工具底层依赖了不同的解析器生成 AST,比如 eslint 使用了 espree、babel 使用了 acorn对每个依赖模块产出一个唯一的 ID,方便后续读取模块相关内容;将每个依赖以及经过 Babel 编译过后的内容,存储在一个对象中进行维护;遍历上一步中的对象,构建出一个依赖图(Dependency Graph);将各模块内容 bundle 产出 个人理解 webpack 是是 npm 的工具模块,是一个 JS 应用打包器, 它将应用中的各个模块打包成一个或者多个 bundle 文件。 借助 loaders 和 plugins,它可以改变、压缩和优化各种各样的文件。 输入不同资源,比如:html、css、js、img、font 文件等,然后将它们输出浏览器可以正常解析的文件。

以上就是我对 webpack 的简单理解,但要理解 webpack 到底是什么,一定要弄清楚下面两个词:

模块化打包 什么是模块化?

1.模块化开发是一种管理方式,是一种生产方式,一种解决问题的方案。他按照功能将一个软件切分成许多部分单独开发,然后再组装起来,每一个部分即为模块。 2.当使用模块化开发的时候可以避免刚刚的问题,并且让开发的效率变高,以及方便后期的维护。

为什么需要模块化?

1.现今的很多网页其实可以看做是功能丰富的应用,它们拥有着复杂的 JavaScript 代码和一大堆依赖包。 2.当一个项目开发的越来越复杂的时候,你会遇到一些问题:命名冲突(变量和函数命名可能相同),文件依赖(引入外部的文件数目、顺序问题)等。 3.JavaScript 发展的越来越快,超过了它产生时候的自我定位。这时候 js 模块化就出现了

模块化进程: 发展一:早期:script 标签

这是最原始的 JavaScript 文件加载方式,如果把每一个文件看做是一个模块,那么他们的接口通常是暴露在全局作用域下,也就是定义在 window 对象中。 缺点:1.污染全局作用域 2.只能按 script 标签书写顺序加载 3.文件依赖关系靠开发者主观解决 发展一:CommonJS 规范 允许模块通过 require 方法来同步加载(同步意味阻塞)所要依赖的其他模块,然后通过 module.exports 来导出需要暴露的接口。

// module add.js module.exports = function add (a, b) { return a + b; } // main.js var {add} = require('./math'); console.log('1 + 2 = ' + add(1,2);

CommonJS 是以在浏览器环境之外构建 JavaScript 生态系统为目标而产生的项目,比如在服务器和桌面环境中。

发展二:AMD/CMD

(1)AMD

AMD 是 RequireJS 在推广过程中对模块定义的规范化产出(异步模块定义)。

AMD 标准中定义了以下两个 API:

require([module], callback); define(id, [depends], callback); require接口用来加载一系列模块,define接口用来定义并暴露一个模块。 define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好 a.add1() ... b.add2() ... })

优点: 1、适合在浏览器环境中异步加载模块 2、可以并行加载多个模块

(2)CMD

CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。(在 CommomJS 和 AMD 基础上提出)

define(function (requie, exports, module) { //依赖可以就近书写 var a = require('./a'); a.add1(); ... if (status) { var b = requie('./b'); b.add2(); } }); 优点: 依赖就近,延迟执行 可以很容易在服务器中运行

(3)AMD 和 CMD 的区别:

AMD 和 CMD 起来很相似,但是还是有一些细微的差别:

1.对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。 2.AMD 推崇依赖前置;CMD 推崇依赖就近,只有在用到某个模块的时候再去 require。 3、AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一

发展三:ES6 模块(ESModule) ECMAScript 2015标准增加了JavaScript语言层面的模块体系定义(关键字)。 在 ES6 中,我们使用export关键字来导出模块,使用import关键字引用模块。 // module math.jsx export default class Math extends React.Component {} // main.js import Math from "./Math"; ES6 模块与 CommonJS 模块的差异:

CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。 CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。 即 CommonJS 先加载整个模块,输出一个对象,取对象内相应的值,输出后内部不会再变化;ES6 是静态编译命令,先加载一个引用,等执行时再根据引用到加载模块内取值输出,动态引用不缓存。 目前只有很少的 JS 引擎能直接支持 ES6 标准,因此 Babel 的做法实际上是将不被支持的 import 翻译成目前已被支持的 require。

发展四:后模块化的编译时代

由于目前很少JS引擎能直接支持 ES6 标准,但是为了让我们的新代码也能运行在用户的老浏览器中,社区涌现了越来越多的工具,他们能静态将高版本规范的代码编译为低版本规范的代码,最为大家所熟知的就是babel。

Babel 的做法可以将不被支持的 import 翻译成目前已被支持的 require。

它把 JS Core 中高版本规范的语法,也能按照相同语义在静态阶段转化成为低版本的语法,这样即使是早期的浏览器,他们内置的 JS 解释器也能看懂。

然而不幸的是,对于模块化相关的import和export关键字,babel最终会将它编译为包含require和exports的CommonJS规范。

这就造成了另一个问题,这样带有模块化关键词的模块,编译之后还是没办法直接运行在浏览器中,因为浏览器端并不能运行 CommonJS 的模块。所以编译这一步并不能帮我们解决,模块化通用的问题。

那么,我们该怎么解决模块化通用的问题呢?

一来,我们怎么把 ESModule 里面的 import 和 export 运用在各个地方;

二来,就是如何让我们在各个地方共同的使用我们的 AMD、CommonJS 等的模块化规范,那我们需要的步骤就是打包。

所以,为了能在 WEB 端直接使用 CommonJS 规范的模块, 除了编译(babel)之外我们还需要一个步骤叫做 打包(bundle)

二、打包 模块化打包工具:

webpack

Rollup

Parcel

fis

打包工具要解决的问题:

文件依赖管理 梳理文件之间的依赖关系

资源加载管理 处理文件的加载顺序(先后时机)和文件的加载数量(合并、嵌入、拆分)

效率与优化管理 提高开发效率,完成页面优化

webpack 打包的规则:

一个入口文件对应一个 bundle。该 bundle 包括入口文件模块和其依赖的模块。

按需加载的模块或需单独加载的模块则分开打包成其他的 bundle。

除了这些 bundle 外,还有一个特别重要的 bundle,就是 manifest.bundle.js 文件,即 webpackBootstrap。

这个 manifest 文件是最先加载的,负责解析 webpack 打包的其他 bundle 文件,使其按要求进行加载和执行。

webpack 底层是如何处理打包的? 1. 参考 Node.js 源码来熟悉 CommonJS 的处理方式 我们可以参考Node.js对于CommonJS模块的处理方式来处理一个CommonJS模块。 在Node.js中,所有的CommonJS模块都会被包裹在一个函数中,然后在node.js中使用vm(虚拟机,http://nodejs.cn/api/vm.html)来运行它,最终达到一个模块化导入和导出的目的。 好比我们执行了 node index.js 执行的时候,node会通过文件系统读取index.js里面的内容,这时候是一个字符串,同时对这个字符串进行一个包裹。通过一个函数字符串的形式,将这个文件的内容包裹进去。把它变成了一个字符串的函数。 1. 首先,当它加载进来一个模块之后,它确定我们要执行哪个commonJS模块之后,node会通过文件系统(fs)读取index.js里面的内容,会在上面和下面加入函数字符串,这样在里面就可以使 用require和exports了,变成了一个函数,也有了参数。 2. 然后,将字符串变成可执行的函数,很多种方式eval、new Function之类的,但node中直接调用vm的模块,这个模块和fs、path一样是一个内置模块。作用和new Function、eval类型,就 是把字符串变成可执行的函数。Node中将字符串放入runInNewContent或者runInThisContent之类的方法就可以变成一个可以执行的函数。 3. 同时,注入进去require和exports等的内容。 4. 之后,就可以在模块之间进行导入和导出了。 **这就是Node.js中如何进行CommonJS操作的流程。**

上代码:

const str = `require('./moduleA'); const str = require('./moduleB'); console.log(str);`; const functionWrapper = ["function(require, module, exports) {", "}"]; // 将我们的文件进行包裹,成为一个字符串函数 const result = functionWrapper[0] + str + functionWrapper[1]; const vm = require("vm"); vm.runInNewContext();

比较难想到的(Node.js中比较核心的就是这一步):

如何将 require、exports 注入进每一个模块。如何将 CommonJS 模块变成一个可执行文件这个是比较难想到的。VM 模块就是调用 V8 相关的接口将我们的字符串变成一个真正可执行的函数。如何将一个 CommonJS 的模块变成一个可执行的函数呢?从而把他们执行呢?其实就是 VM 这一层做的。 2. 浏览器中对 CommonJS 的处理

我们在浏览器中也可以用相同的思路进行处理:

我们在打包阶段将每个模块包裹上一层函数字符串,然后放置到浏览器中去执行它。 同时我们实现一个简单版本的 require 函数和 module 对象来处理运行时加载的问题,这样一个基本流程就好了。 接下来我们要处理运行时模块之间的依赖关系,所以我们需要自己维护一个。

我们要做一个东西,怎么把 CommonJS 的规范运行在浏览器里面去?

逆推的思路来想这个事情。怎么结合刚才讲的 CommonJS 模块的原理。

思考一下,假如我们要把这个 index.js 模块放在浏览器里面运行,需要做哪些东西呢?

我们可以先手动写 bundle 文件:

思路:

首先,放到作用域里面去,要解决变量提升、函数提升等作用域冲突。需要先定义一个自执行函数,函数作用域是比较稳定的;

然后,我们仿照 CommonJS 的步骤,对模块进行包裹。将我们 index.js 的模块用函数包裹的形式包裹起来,让它出现在打包的结果里面,这样至少执行的时候不会报错了,因为注入了变量。

接下来,是我们怎么注入变量,之前说到我们要实现 require 函数和 module.exports 对象,我们先实现一个 module 对象吧,同时里面有 exports 方法。

而 require 函数是加载模块用的,实际接收一个 id,通过 id 去找其他模块。

结合用 webpack 打包后的 bundle.js 内的代码,举例来验证:

文件目录结构:

代码块

(function (modules) { // 打包成了一个自执行函数 var installedModules = {} // 缓存 function __webpack_require__(moduleId) { // 模拟了一个require方法 原理:通过递归的方式不停的调用自己 if (installedModules[moduleId]) { return installedModules[moduleId].exports } var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} } modules[moduleId].call(module.exports, module, module.exports, __webpack_require__) module.l = true return module.exports } // return __webpack_require_((__webpack_require_.s = "./main.js")) return __webpack_require__(0) })({ // 0 key:index.js value: 是一个函数 "./index.js": (function (module, exports) { eval( 'import a from "./a";\n\nconsole.log("hello word");\n\n\n//#sourceURL=webpack:///./index.js?' ), "./a.js": function (module,exports) { eval( '// import b from "./b";\n\nconsole.log("hello word");\n\n\n//#sourceURL=webpack:///./index.js?' ), "./c.js": function (module,exports) { eval( '// \n\nconsole.log("hello word");\n\n\n//#sourceURL=webpack:///./index.js?' ), "./d.js": function (module,exports) { eval( '// \n\nconsole.log("hello word");\n\n\n//#sourceURL=webpack:///./index.js?' ), }, "./b.js": function (module,exports) { eval( '// import c from "./c";\n\nconsole.log("hello word");\n\n\n//#sourceURL=webpack:///./index.js?' )} }) })

它借助了一个__webpack_require函数来实现自己的模块化,把代码都存放在installedModules,代码文件以对象形式传递进来,key 是文件的路径(需要打包的文件),value是一个函数,通过eval()执行当前文件的代码。value可以理解为:包裹代码的字符串,并且代码内部的require,都被替换成了__webpack_require__。

咱们来分析下上述代码的运行机制:

打包出来的 bundle.js 是一个 IIFE (立即调用函数表达式)modules 是一个对象,每个 key 对应的是一个模块函数函数 webpack_require 加载模块,返回 module.exportswebpack 中每个模块都有一个唯一的 id,是从 0 开始递增的,即从入口文件开始。通过 webpack_require(0) 启动程序

我们实际上就是把 index.js 里面的这个模块用函数包裹了一下,然后 mock 了一个 module,它就可以运行了。模块内容其实是没有变化的,只是包裹在了一个函数里面,同时执行了它。

node index.bundle.js 执行成功,然后放到浏览器里面执行也可以,这个时候说明这个模块他就是一个环境无关的代码了,经过我们的这么一个处理之后,就不用再关心它有没有 module.exports 这种 CommonJS 规范了。

咱们对这个立即执行函数进行简单的理解:

(闭包函数)(以入口文件为首的需要打包的文件们)

对闭包函数部分进行分析:

首先它接收一个 id,同时通过闭包的形式把 currentModuleId 也传入进去,这样就能让每一个 require 函数都知道是由哪一个模块进入这个模块的,最终返回结果。 这个闭包的作用就是当我 require index.js 的时候,我应该去 modules 里面的哪个下标来去找这个对应关系

总结:

webpack 支持所有符合 ES5 标准 的浏览器(不支持 IE8 及以下版本)。

对 Webpack 的使用者来说,它是一个简单强大的工具,对 Webpack 的开发者来说,它是一个扩展性的高系统。

Webpack 之所以能成功,在于它把复杂的实现隐藏了起来,给用户暴露出的只是一个简单的工具,让用户能快速达成目的。 同时整体架构设计合理,扩展性高,开发扩展难度不高,通过社区补足了大量缺失的功能,让 Webpack 几乎能胜任任何场景。

Webpack 是一个庞大的 Node.js 应用,如果你阅读过它的源码,你会发现实现一个完整的 Webpack 需要编写非常多的代码。但你无需了解所有的细节,只需了解其整体架构和部分细节即可。

Node.js 从最一开始就支持模块化编程。然而,在 web,模块化的支持正缓慢到来。在 web 存在多种支持 JavaScript 模块化的工具,这些工具各有优势和限制。webpack 基于从这些系统获得的经验教训,并将模块的概念应用于项目中的任何文件。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有